/* editor-panel.vala
 *
 * Copyright (C) 2010  Richard Talbot-Watkins
 * Copyright (C) 2010  Dave Jeffery
 * 
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Authors:
 *  Richard Talbot-Watkins <richtw1@gmail.com>
 * 	Dave Jeffery <david.richard.jeffery@gmail.com>
 */

using Gtk;
using Cairo;

/**
 * The EditorPanel widget allows you to edit the sprites contained in
 * the current sprite file. 
 *
 * @author Richard Talbot-Watkins <richtw1@gmail.com>
 * @author Dave Jeffery <david.richard.jeffery@gmail.com>
 * @version 2010.1218
 * @since 0.1
 */
private class MonsterDesigner.EditorPanel : DrawingArea {
	/**
	 * Horizontal zoom factor in pixels.
	 */
    private const int X_ZOOM = 24;

	/**
	 * Vertical zoom factor in pixels.
	 */
    private const int Y_ZOOM = 12;

	/**
	 * Toolbox edit mode.
	 */
	private ToolBoxMode edit_mode;

	/**
	 * Grid status.
	 */
	private GridState grid_state;

	/**
	 * Sprite data.
	 */
	private SpriteData sprite_data;

	/**
	 * Palette data.
	 */
	private PaletteData palette_data;

	/**
	 * Used to store the current draw and erase colours.
	 */
	private PaintColours paint_colours;

	/**
	 * Used to store the mouse recent mouse button pressed.
	 */
	private MouseButton mouse_button;

	/**
	 * Used to store the current position of the mouse.
	 */
	private Point mouse_position;

	/**
	 * Used by the draw tool to store the original position of the mouse.
	 */
	private Point last_mouse_position;

	/**
	 * Used to keep track of mouse drags.
	 */
	public bool is_mouse_held = false;

	/**
	 * The origin of the selection rectangle - i.e. the first point that was 
	 * clicked.
	 */
	private Point selection_start;

	/**
	 * The current end point of the selection. This still may be getting
	 * updated if the drag is continuing.
	 */
	private Point selection_end;

	//TODO: Make getter/setter
	public bool is_selection = false;

	//TODO: Make getter/setter
	public bool is_paste_preview = false;

	private Point paste_position;

	private Clipboard clipboard;

	/**
	 * Constructor for the EditorPanel widget.
	 * 
	 * @param edit_mode current sprite editing mode
	 * @param grid_state current grid state
	 * @param sprite_data sprite data
	 * @param palette_data The palette used to render the sprite data.
	 * @param paint_colours The current draw and erase colours.
	 */ 
	public EditorPanel (ToolBoxMode edit_mode, GridState grid_state, 
						SpriteData sprite_data, PaletteData palette_data,
						PaintColours paint_colours) {
		this.edit_mode = edit_mode;
		this.grid_state = grid_state;
		this.sprite_data = sprite_data;
		this.palette_data = palette_data;
		this.paint_colours = paint_colours;

        add_events (Gdk.EventMask.BUTTON_PRESS_MASK
                  | Gdk.EventMask.BUTTON_RELEASE_MASK
                  | Gdk.EventMask.POINTER_MOTION_MASK);

		set_size_request (315, 623);

		/* Redraw the widget whenever the grid state changed. */
		grid_state.changed.connect (redraw_canvas);

		/* Redraw the widget whenever the edit mode changed. */
		edit_mode.changed.connect (on_edit_mode_change);

		/* Redraw the widget whenever the palette changed. */
		palette_data.changed.connect (redraw_canvas);

		/* Redraw the widget whenever the sprite data changed. */
		sprite_data.changed.connect (redraw_canvas);
	}

	/**
	 * Called whenever the edit mode is changed.
	 */
	public void on_edit_mode_change () {

		/* Changing edit mode always clears the selection. */
		is_selection = false;

		/* Changing edit mode always clears paste preview. */
		is_paste_preview = false;

		/* Get rid of any rectangle or paste preview artifacts. */
		redraw_canvas ();
	}

	/**
	 * Event sent when the mouse is pressed over the EditorPanel widget.
	 *
	 * @param event Used to work out mouse button and position.
	 * @return false to allow other handlers to receive this signal.
	 */
    public override bool button_press_event (Gdk.EventButton event) {
		/* Store mouse button and position. */
		mouse_position = sprite_position_from_mouse (event.x, event.y);
		mouse_button = (MouseButton) event.button;

		switch (edit_mode.mode) {
			case Mode.DRAW:
				do_draw ();
				break;
			case Mode.FILL:
				do_fill ();
				break;
			case Mode.INSERT_ROW:
				do_insert_row ();
				break;
			case Mode.DELETE_ROW:
				do_delete_row ();
				break;
			case Mode.SELECT:
				do_select ();
				break;
			case Mode.PASTE:
				do_paste ();
				break;
		}

        return false;
    }

	/**
	 * Event sent when the mouse is moved over the EditorPanel widget.
	 *
	 * We use this to see whether the mouse is pressed and held, or dragged.
	 */
    public override bool motion_notify_event (Gdk.EventMotion event) {

		switch (edit_mode.mode) {
			case Mode.DRAW:
			    if (is_mouse_held) {
					mouse_position = sprite_position_from_mouse (
														event.x, event.y);
					do_draw ();
					last_mouse_position = mouse_position;
			    }
				break;
			case Mode.SELECT:			
				if (is_mouse_held && mouse_button == MouseButton.LEFT) {
					mouse_position = sprite_position_from_mouse (
														event.x, event.y);
					//TODO Add automatic scrolling.

					selection_end = mouse_position;
					redraw_canvas ();
				}
				break;
			case Mode.PASTE:
				if (clipboard != null) {
					paste_position = sprite_position_from_mouse (
														event.x, event.y);
					is_paste_preview = true;
					redraw_canvas ();
				}
				break;
		}

        return false;
    }

	/**
	 * Event called when the mouse is released over the EditorPanel widget,
	 * so we can forget about drags.
	 */
	public override bool button_release_event (Gdk.EventButton event) {
		is_mouse_held = false;
		return false;
	}

	/**
	 * Called in draw mode to set pixels in the sprite.
	 *
	 * A line is interpolated between the last known mouse position and the
	 * one passed in.
	 */
	private void do_draw () {
		is_mouse_held = true;
		last_mouse_position = mouse_position;

		/* Use either the draw or erase colour. */
		var logical_colour = colour_for_button (mouse_button);

		/* Draw a line. */
		sprite_data.draw_line (mouse_position, last_mouse_position,
								logical_colour);
	}

	/**
	 * Called to do a flood fill.
	 */
	private void do_fill () {
		is_mouse_held = true;

		/* Use either the draw or erase colour. */
		var logical_colour = colour_for_button (mouse_button);

		/* Fill. */
		sprite_data.fill (mouse_position, logical_colour);
	}

	/**
	 * Called when in insert mode to insert a row into the bitmap.
	 */
	private void do_insert_row () {
		/* Use either the draw or erase colour. */
		var logical_colour = colour_for_button (mouse_button);

		/* Insert row.*/
		sprite_data.insert_row (mouse_position.y, logical_colour);
	}

	/**
	 * Called when in delete mode to delete the clicked row.
	 */
	private void do_delete_row () {
		/* Use either the draw or erase colour. */
		var logical_colour = colour_for_button (mouse_button);

		/* Delete row.*/
		sprite_data.delete_row (mouse_position.y, logical_colour);
	}

	/**
	 * Called when in select mode to begin a new selection.
	 */
	private void do_select () {
		is_mouse_held = true;
		is_selection = true;

		if (mouse_button == MouseButton.LEFT) {
			selection_start = mouse_position;
			selection_end = selection_start;
		}
	}

	/**
	 * Called when in paste mode to paste clipboard into the bitmap.
	 */
	private void do_paste () {
		/* Make sure that a paste should be performed. */
		if (mouse_button != MouseButton.LEFT || clipboard == null) {
			return;
		}

		sprite_data.set_area (mouse_position, clipboard);
	}

	/**
	 * Called when Select All activated on Select menu.
	 */
	public void do_select_all () {
		is_selection = true;
		selection_start = Point (0, 0);
		selection_end = Point (sprite_data.width, sprite_data.height);
		redraw_canvas ();
	}

	/**
	 * Called when Select None activated on Select menu.
	 */
	public void do_select_none () {
		is_selection = false;
		redraw_canvas ();
	}

	/**
	 * Called when Flip Horizontally activated on Select menu.
	 */
	public void flip_horizontal () {
		/* If there's nothing to add, do nothing. */
		if (!is_selection) { return; }

		var box = get_selection_bounds (selection_start, selection_end);
		sprite_data.flip_horizontal (box);
	}

	/**
	 * Called when Flip Vertically activated on Select menu.
	 */
	public void flip_vertical () {
		/* If there's nothing to add, do nothing. */
		if (!is_selection) { return; }

		var box = get_selection_bounds (selection_start, selection_end);
		sprite_data.flip_vertical (box);
	}

	/**
	 * Called when Copy activated on Select menu.
	 */
	public void add_to_clipboard () {
		/* If there's nothing to add, do nothing. */
		if (!is_selection) { return; }

		var box = get_selection_bounds (selection_start, selection_end);
		var contents = sprite_data.get_area (box);

		clipboard = new Clipboard (box.width, box.height, contents);
	}


	/**
	 * Converts a mouse position into a position within the sprite in MODE5
	 * pixels.
	 *
	 * @param mx Mouse x position in pixels.
	 * @param my Mouse y position in pixels.
	 * @return Mouse position in MODE5 pixels.
	 */
	private Point sprite_position_from_mouse (double mx, double my)  {
		/* Get sprite pixel coordinates from mouse coordinates.*/
		var x = (int) mx / X_ZOOM;
		var y = (int) my / Y_ZOOM;
		x = x.clamp (0, sprite_data.width);
		y = y.clamp (0, sprite_data.height);

		return Point (x, y);
	}

	/**
	 * Uses the mouse button value to work out which logical colour
	 * to use for operation - either draw (left) or erase (right).
	 *
	 * @param mouse_button A Gdk mouse button code.
	 * @return A MODE5 logical colour in the range 0 - 3.
	 */
	private int colour_for_button (MouseButton mouse_button) {
		/* Use either the draw or erase colour. */
		var logical_colour = paint_colours.draw_colour;

		if (mouse_button == MouseButton.RIGHT) {
			logical_colour = paint_colours.erase_colour;
		}

		return logical_colour;
	}

	/**
	 * Returns the sprite pixel bounds of the selected area
	 * 
	 * @param start Start of selected area (in MODE5 pixels).
	 * @param end End of selected area (in MODE5 pixels).
	 * @return Rectangle object containing top left corner, width and height.
	 */
	public Rectangle get_selection_bounds (Point start, Point end) {

		/* Make sure selection starts at top left. */
		var x = int.min (start.x, end.x);
		var y = int.min (start.y, end.y);

		/* The 1 adjusts for the fact that pixels are in the range 0 - n, 
		   but widths and heights should be in the range 1 - n+1. */
		var width = 1 + (start.x - end.x).abs ();
		var height = 1 + (start.y - end.y).abs ();

		/* Make sure width and height values won't take selection box off 
		   the edge of the canvas. */
		width = width.clamp (0, sprite_data.width - x);
		height = height.clamp (0, sprite_data.height - y);

		return Rectangle (x, y, width, height);
	}

	/**
	 * Expose event handler. 
	 *
	 * The expose event is emitted when the 
	 * widget is asked to (re)paint itself.
	 * 
	 * @return false to pass signal to the default handler
	 */
	public override bool expose_event (Gdk.EventExpose event) {
		var cr = Gdk.cairo_create (this.window);
		cr.rectangle (event.area.x, event.area.y,
						event.area.width, event.area.height);
		cr.clip ();
		draw (cr);
		return false;
	}

	/**
	 * This method is called when the widget wants to repaint 
	 * itself.
	 */
	private void redraw_canvas () {
		if (this.window == null) {
			return;
		}

		set_size_request (315, sprite_data.height * Y_ZOOM);

		unowned Gdk.Region region = this.window.get_clip_region ();
		this.window.invalidate_region (region, true);
		this.window.process_updates (true);
	}

	/**
	 * Draws the widget using the Cairo graphics library.
	 *
	 * @param cr Cairo context to draw onto
	 */
	private void draw (Context cr) {
		cr.set_antialias(Antialias.NONE);

		cr.save ();

		/* Draw zoomed up sprite, pixel by pixel */
		for (int y = 0; y != sprite_data.height; ++y) {
			for (int x = 0; x != sprite_data.width; ++x) {
				var rgb = sprite_data.get_pixel (Point (x, y), palette_data);
				if (is_paste_preview) {
					var target = Point (x - paste_position.x, 
										y - paste_position.y);
					var c = clipboard.get_pixel (target, palette_data);
					rgb = (c == null) ? rgb : c;
				}
				cr.set_source_rgb (rgb.red, rgb.green, rgb.blue);
				cr.rectangle (x * X_ZOOM, y * Y_ZOOM, X_ZOOM, Y_ZOOM);
				cr.fill ();
			}
		}

		/* Draw the grid if necessary. */
        if (grid_state.enabled) {
            cr.set_source_rgb (0.4d, 0.4d, 0.4d);
			cr.set_line_width (0.5 * cr.get_line_width ());
            for (int x = 0; x != sprite_data.width; ++x) {
				cr.move_to (x * X_ZOOM, 0);
				cr.line_to (x * X_ZOOM, sprite_data.height * Y_ZOOM);
				cr.stroke ();
            }

			for (int y = 0; y != sprite_data.height; ++y) {
				cr.move_to (0, y * Y_ZOOM);
				cr.line_to (sprite_data.width * X_ZOOM, y * Y_ZOOM);
				cr.stroke ();
			}
		}

        /* Always draw the divider lines between each row. */
        cr.set_source_rgb (0.6d, 0.6d, 0.6d);
		cr.set_line_width (1.5 * cr.get_line_width ());
        for (int y = 0; y != sprite_data.height; ++y) {
            if ((y & 7) == 0) {
				cr.move_to (0, y * Y_ZOOM);
				cr.line_to (sprite_data.width * X_ZOOM, y * Y_ZOOM);
				cr.stroke ();
            }
        }

		/* Draw the selected rectangle.*/
		if (is_selection) {
			var box = get_selection_bounds (selection_start, selection_end);

			cr.set_source_rgba (0.5d, 0.5d, 0.5d, 0.5d);
			cr.rectangle (box.x * X_ZOOM, box.y * Y_ZOOM, 
							box.width * X_ZOOM, box.height * Y_ZOOM);
			cr.fill ();

            cr.set_source_rgb (0.75d, 0.75d, 0.75d);
			cr.set_line_width (0.75 * cr.get_line_width ());
			cr.rectangle (box.x * X_ZOOM, box.y * Y_ZOOM, 
							box.width * X_ZOOM, box.height * Y_ZOOM);
			cr.stroke ();
		}

		cr.restore();
	}
}
